Skip to content

gh-159434: Avoid borrowed references in func_get_annotation_dict() - #159436

Draft
nascheme wants to merge 1 commit into
python:mainfrom
nascheme:gh-159434-func-annotate-stale
Draft

nascheme wants to merge 1 commit into
python:mainfrom
nascheme:gh-159434-func-annotate-stale

Conversation

@nascheme

@nascheme nascheme commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Calling a function's __annotate__ can run arbitrary code that replaces __annotate__ or __annotations__, either directly or, on the free-threaded build, from another thread while the critical section is suspended. The callable was used through a borrowed reference for the duration of the call, and the compiler-generated annotations tuple was read through a borrowed reference while building the dict. Hold strong references to both.

If __annotations__ was set while __annotate__ was running, keep that value instead of overwriting it with the result of the call.

Note that this does not try to fix the "stale value" issue as reported in the bug. E.g. if __annotations__ get replaced when __annotate__ runs, we don't re-run __annotate__ again. I can't think of a legitimate reason someone would need that. Also, re-running could result in func_get_annotation_dict() potentially looping forever.

Why keep the existing value for __annotations__? On the free-threaded build, two threads can read f.__annotations__ for the first time concurrently. If the first thread's critical section is suspended while it runs __annotate__, the second thread can compute, cache and return its own dict. Overwriting that dict when the first thread resumes would lose any in-place changes the second thread's caller made (e.g. a decorator doing f.__annotations__["return"] = X). It would also free a dict that a PyFunction_GetAnnotations() caller may still hold as a borrowed reference. Keeping the existing value means that once a dict has been handed out, it stays cached until it is explicitly replaced.

Creation of this PR was assisted by Claude Opus 5.5.

Calling a function's __annotate__ can run arbitrary code that replaces
__annotate__ or __annotations__, either directly or, on the
free-threaded build, from another thread while the critical section is
suspended.  The callable was used through a borrowed reference for the
duration of the call, and the compiler-generated annotations tuple was
read through a borrowed reference while building the dict.  Hold strong
references to both.

If __annotations__ was set while __annotate__ was running, keep that
value instead of overwriting it with the result of the call.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant